-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
11조 과제 제출 (박진영, 남기훈, 이정우) #7
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요, 2조 윤금엽입니다! axios interceptor, cloudinary를 비롯해 README를 상세하게 작성해주셔서 네트워크 통신 등에 대한 공부가 많이 필요하겠다는 생각을 하게 됐습니다😭 setTimeOut() 함수 등으로 요청 처리·속도에 대해서도 신경쓰신 것 같아 많이 배우게 된 것 같습니다! 이번 프로젝트 수고 많으셨습니다🙌🏻✨
timeout: 5000, | ||
}); | ||
|
||
customAxios.interceptors.request.use( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
axios의 interceptor api를 활용해서 then이나 catch로 요청 처리 전 요청과 응답을 처리할 수 있는 기능을 덕분에 알게 됐네요! 다른 프로젝트에서 공식 문서를 참고해서 사용해 봐야겠습니다:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
리뷰 달아주셔서 감사합니다!
const currentTime = Math.floor(new Date().getTime() / 1000); | ||
// 만료시간 > 5분 남은 경우 | ||
const expirationLeft = expirationTime - currentTime; | ||
console.log(`만료 ${Math.floor(expirationLeft / 60)}분 남았습니다.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
만료 시간을 확인하는 console.log가 모든 페이지에서 반복적으로 출력되고 있어서 확인해주시면 좋을 것 같습니다!
async (error) => { | ||
const status = error.response?.data.error.status; | ||
|
||
if (status === 401) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
조건문이 길어서 유지·보수가 어려울 수도 있을 것 같다는 생각이 드는데 협업하시는 동안 불편하지 않으셨는지 궁금합니다! hooks 분리를 하거나 조건문 내에서 반복되고 있는 response.data.response.accessToken
을 변수로 저장해보시는 건 어떨까요?
createAt: string; | ||
} | ||
|
||
export const DUMMY_WORKERS: DataType[] = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
commit에 더미데이터를 push할 경우 프로젝트 기능을 시연할 때 용이하고, 리팩토링 유지보수 및 전·후 변경 사항을 검증할 수 있는 장점이 있다고 알고 있는데 이러한 목적으로 push를 하신 게 맞으실까요? 혹시 다른 이유가 있으시다면 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안지우고 푸시했습니다.
src/page/signup/signup.tsx
Outdated
message.error('이메일 인증 실패!'); | ||
} | ||
} catch (error) { | ||
message.error('서버 요청에 실패하였습니다.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이메일 인증번호를 발송한 후 이메일을 변경했을 때
변경한 이메일로 재발송을 하기 위해선 회원가입 페이지를 재렌더링하거나 시간만료 후 재발송 버튼을 클릭했을 때만 가능한 것 같습니다! 사용자가 오타를 입력한 상황이라면 재요청을 해야 할 경우가 생길 것 같은데 혹시 어떻게 생각하시나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오타에 대해서 미처 고려하지 못 한 부분이 있었네요. 일단 최종적으로 재인증 기능을 넣긴했는데 , 처음부터 오탈자에 대해서 염두를 하지 않은 부분이었네요. 이 부분 추가로 보완해서 오탈자에 대해서 대처 할 수 있게 하겠습니다.
}} | ||
> | ||
<Avatar src={userHeaderInfo.profileThumbNail} /> | ||
<Link to="/myaccount">{userHeaderInfo.userName}</Link> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'내 정보', '내 연차/당직', '관리자' 기능이 있는 페이지(일명 마이 페이지)로 이동하는 메뉴가 가시적으로 잘 보이지 않아서 헤매게 되는 것 같습니다! 메인 페이지에서 쉽게 확인할 수 있도록 Header의 가운데로 메뉴를 옮겨보시는 건 어떨까요?
disabledDate={pastDates} | ||
/> | ||
) : ( | ||
<DatePicker |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
App.tsx에서 <ConfigProvider theme={theme} locale={koKR}>
locale 속성을 추가해주셨기 때문에 DatePicker에서 년도는 한글화가 되어있지만 월과 요일이 여전히 영어로 출력되고 있습니다! ant design은 dayjs 라이브러리를 기본적으로 사용하고 있기 때문에 dayjs도 한글화를 해줘야 DatePicker의 요소들이 모두 한글로 변환이 잘 되더라구요! 이 내용을 추가해주시면 될 것 같습니다:)
import dayjs from 'dayjs';
import 'dayjs/locale/ko';
import locale from 'antd/locale/ko_KR';
// dayjs 라이브러리 한글화
dayjs.locale('ko');
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요! 2조 김경원입니다! 감사하게도 README 파일을 엄청 자세히 작성해주시고, 주석도 잘 달아주셔서 코드를 이해하는데 큰 도움이 되었습니다😊. 이번 프로젝트를 진행하면서 놓치고 신경 못썼던 부분에 대해 알게되고, 저희 프로젝트와 차이점을 보는게 흥미로워 즐겁게 리뷰했습니다! 리뷰를 진행하면서 refresh 토큰 및 cookie보안 등, 중요한 부분을 신경쓰지 못한 것 같아 스스로 부끄러웠네요.😅
많이 배울 수 있었던 시간이었습니다. 개 쩌는 팀이군요, 수고 많으셨습니다~!
headers: { 'Content-Type': 'application/json' }, | ||
}); | ||
return response; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
인증 메일 발송 api는 백엔드 측에서 작업한 것 같은데, 메일 내용을 보니까, 백엔드쪽에서 메일 인증 관련 라이브러리를 사용하는 것 같은데 맞을까요? 저는 생각만 해보고 복잡할 것 같아서 넘겼는데, 실제로 작동하는게 놀랍습니다!👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
넵, 백엔드 쪽에서 작업한 내용인데 인증 관련 라이브러리를 사용했다고 들었습니다.
locale={'ko'} // 지역 | ||
dayCellContent={renderDayCellContent} // '일' 문자 렌더링 변경 | ||
ref={calendarRef} | ||
headerToolbar={false} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희도 fullCalendar 를 사용했는데, 캘린더 조작 부분에서 많이 헤매었던 기억이 납니다.😂
headerToolbar를 false로 두고 아예 커스텀하는 방법도 있군요. antDesign 사용하시면서 ui/ux적으로 많이 고민하신 흔적이 느껴져서 좋습니다.
} | ||
}; | ||
getUsersYearlySchedules(); | ||
}, [year, accessToken, userEmail]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
home.tsx에서 관리하는 상태들이 많은 것 같은데, setEvents나 scheduleList api활용 관련 코드는 calendar.tsx 컴포넌트로 분리하여 관리하셔도 좋을 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
모든 일정 또는 사용자의 승인된 일정을 렌더링하는 calendar컴포넌트와 사이드바에서 사용자의 신청 대기 상태와 신청 결과 상태를 확인할 수 있는 mySchedule 컴포넌트가 home컴포넌트의 하위 컴포넌트라 home컴포넌트에서 1년치 데이터를 받아와서 calendar와 mySchedule에 전달해주고 있는 구조입니다.
return current < dayjs().startOf('day'); | ||
}; | ||
|
||
const mySchedule = events.filter((event) => event.userEmail === userEmail); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
개인적인 의견이지만 나의 일정 확인에서는 단순 전체 목록 필터링이 아닌,
내 연차/당직 확인 api 데이터와 연차 / 당직 내 신청 목록 api 데이터(맞는 api인지는 잘 모르겠습니다!😅)도 캘린더에 보여주시면 유저들이 더 직관적으로 이해할 수 있을 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희는 모든 사용자의 연차/당직 스케쥴 1년치 데이터를 받아와서 승인 상태의 데이터만 달력에 렌더링 되게 하였습니다.
그리고 필터링을 통해 신청 목록, 그러니까 대기 중인 상태는 메인페이지에 사이드바에서 요청대기 리스트에
승인 또는 거절 상태는 요청결과 리스트에 표시 중에 있습니다. 이 부분들은 모두 사용자의 일정만 표시 됩니다.
불필요한 서버와의 통신을 최대한 줄이기 위해서 1번의 데이터를 통해서 필터링을 이용해 <모든 일정>/<나의 일정>을 스위치로 필터링, <사이드 바 요청대기>, <사이드 바 요청 결과> 이렇게 렌더링 되게 하였습니다.
title: item.userName, | ||
start: item.startDate, | ||
end: adjustEndDate, | ||
color: DUTY_ANNUAL[item.scheduleType].color, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 색깔이 무엇을 의미하는지 Calendar에서 보여주는 지표가 있으면 좋을 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
constants 파일내부에 DUTY_ANNUAL객체에 DUTY와 ANNUAL이 key값으로, 거기에 따른 {labe, color}가 value값으로 있습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 추상적으로 리뷰를 작성했군요 죄송합니다😂. 렌더링되는 화면 안에서 유저가 볼 수 있는 범례가 있으면 좋겠다는 뜻이었습니다..!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/types/IMySchdule.ts
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
취향 차이기는 한것 같지만, 해당 파일처럼 타입들을 한 폴더에서 관리하면 유지 / 보수에 더 용이할 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
원래 그런 용도로 만들 파일이었지만..작업하다 보니 컴포넌트 자체에서 때려박은 경우가 많습니다..😂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요! 11조 담당 멘토 김민수입니다.
리뷰가 조금 늦었습니다. 죄송합니다..
전반적으로 하나의 파일에 모든 스크립트가 쓰여있다보니, 가독성이 떨어지는 점,
any 활용과 불필요한 주석 개선
변수명 개편
불필요하게 반복되는 try catch 문 개선
등이 보여 리뷰로 남겨봤습니다.
serverState 관리를 유용하게 해주는 react-query나 swr 을 활용해봤으면 더 좋았을 것 같습니다.
form 관리도 react-hooks-form 이라는 툴을 활용했다면 조금 더 유연한 상태관리가 가능하니, 다음에 공부해보시는 것도 좋을 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
필요한 파일들만 ignore 하는 건 어떨까요? 지금은 쓰이지 않는 애들도 같이 포함된 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
node_modules
.env
제외 모두 삭제했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제출용 레포지토리라서 추후에 업데이트한 내용이 포함되어있지 않는거 같습니다.
signup 컴포넌트는 무겁고 가독성이 좋지 않아서 분리 작업 완료했습니다.
export const checkEmail = async (userEmail: string) => { | ||
// 추후에 /v1/auth/check-email 변경 | ||
const response = await customAxios.post('/v1/auth/check-email', userEmail, { | ||
headers: { 'Content-Type': 'application/json' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
header를 axiosInstance 생성간 미리 넣어뒀으면 어땠을까요..?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const checkEmail = async (userEmail: string) => {
// 추후에 /v1/auth/check-email 변경
const response = await customAxios.post('/v1/auth/check-email', userEmail);
return response;
};
export const customAxios = axios.create({
baseURL: BASE_API_URL,
timeout: 5000,
headers: { 'Content-Type': 'application/json' },
});
수정했습니다.
|
||
export const checkEmail = async (userEmail: string) => { | ||
// 추후에 /v1/auth/check-email 변경 | ||
const response = await customAxios.post('/v1/auth/check-email', userEmail, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
axios에 제네릭을 활용하면 응답값을 typed 하게 관리할 수 있습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!file) { | ||
return null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
file이 있고 없고는 함수에서 처리할 필요가 없습니다. file의 타입이 정해져있기도 하고, 인자를 제대로 전달해주지 않아서 생기는 이슈는 함수 단에서 제어문을 할 필요가 없습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const currentTime = Math.floor(new Date().getTime() / 1000); | ||
// 만료시간 > 5분 남은 경우 | ||
const expirationLeft = expirationTime - currentTime; | ||
console.log(`만료 ${Math.floor(expirationLeft / 60)}분 남았습니다.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
배포까지 진행되는 프로젝트에서는 로그를 남길 필요가 없고, 개발환경에서만 로그가 남도록 커스텀 로그를 사용하는 것도 방법일 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
개발환경, 배포환경 관리하는 부분 공부해보겠습니다.
일단 해당 로그는 배포 환경에서 주석처리하였습니다.
if (!accessToken) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
엑세스 토큰이 있는지 없는지를 매번 검사해줘야 하나요? customAxios에서 처리해줄 수 있는 부분 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interceptor에서 처리했습니다.
messageApi.open({ | ||
type: 'error', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { message } from 'antd';
import { NoticeType } from 'antd/es/message/interface';
export const useMessage = () => {
const [messageApi, contextHolder] = message.useMessage();
const customMessage = (content: string, type: NoticeType = 'success') =>
messageApi.open({
type,
content,
});
return { customMessage, contextHolder };
};
customMessage('비밀번호를 수정하였습니다.');
customMessage('비밀번호 수정에 실패하였습니다.', 'error');
const NUMBER_REGEX = /\d/; | ||
const SPECIAL_REGEX = /[!@#$%^&*()-+=]/; | ||
const ENGLISH_REGEX = /[a-zA-Z]/; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
정규식은 constants 폴더에서 관리했으면 좋았을 것 같습니다.
const CheckedVacationRequestsData = response.data | ||
.response as CheckedVacationRequestType[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
axios에서 제네릭을 사용했으면 as 키워드를 사용할 필요가 없어집니다.
const pastDates = (current: dayjs.Dayjs) => { | ||
return current < dayjs().startOf('day'); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
boolean 을 return 해주는 함수는 is 키워드를 쓰면 가독성이 좋아집니다.
함수를 작성할 때 호출부를 고려하면 좀 더 나은 함수명을 작성할 수 있습니다.
pastDates(currentDate)
이렇게 쓰이면 무슨 일을 하는지 어떤 결과 값이 나오는지 알 수가 없겠죠,,?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
가독성 떨어지고 난잡한 코드를 꼼꼼히 리뷰해주셔서 감사드립니다.
리뷰해주신 부분 수정, 추가 질문 코멘트 남겼습니다.
시간 되실 때 천천히 봐주시면 정말 감사드리겠습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
node_modules
.env
제외 모두 삭제했습니다.
|
||
export const checkEmail = async (userEmail: string) => { | ||
// 추후에 /v1/auth/check-email 변경 | ||
const response = await customAxios.post('/v1/auth/check-email', userEmail, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const checkEmail = async (userEmail: string) => { | ||
// 추후에 /v1/auth/check-email 변경 | ||
const response = await customAxios.post('/v1/auth/check-email', userEmail, { | ||
headers: { 'Content-Type': 'application/json' }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const checkEmail = async (userEmail: string) => {
// 추후에 /v1/auth/check-email 변경
const response = await customAxios.post('/v1/auth/check-email', userEmail);
return response;
};
export const customAxios = axios.create({
baseURL: BASE_API_URL,
timeout: 5000,
headers: { 'Content-Type': 'application/json' },
});
수정했습니다.
const currentTime = Math.floor(new Date().getTime() / 1000); | ||
// 만료시간 > 5분 남은 경우 | ||
const expirationLeft = expirationTime - currentTime; | ||
console.log(`만료 ${Math.floor(expirationLeft / 60)}분 남았습니다.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
개발환경, 배포환경 관리하는 부분 공부해보겠습니다.
일단 해당 로그는 배포 환경에서 주석처리하였습니다.
import { customAxios } from '@/api/customAxios'; | ||
|
||
export const getVacationRequests = async () => { | ||
const response = await customAxios.get('/v1/admin/list', {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
삭제했습니다.
const options = [ | ||
{ label: '연차', value: 'ANNUAL' }, | ||
{ label: '당직', value: 'DUTY' }, | ||
]; | ||
|
||
const [messageApi, contextHolder] = message.useMessage(); | ||
const [checkedBox, setCheckedBox] = useState(['ANNUAL', 'DUTY']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
위에 'PENDING' | 'APPROVE' | 'REJECT' 와 같은 내용이네요.
수정했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 이부분이 가장 큰 문제라고 생각합니다.
일단 나름의 방식으로 정리를 해봤는데 잘했는지는 의문입니다.
(Home.tsx)
import { Layout, Modal } from 'antd';
import { Content } from 'antd/es/layout/layout';
import { useState } from 'react';
import Calendar from './calendar';
import Signin from '@/page/home/signin';
import {
useAddingSchedule,
useDataFetching,
useLoginModalOpen,
} from './home.hook';
import Sidebar from './sidebar';
export default function Home() {
const [year, setYear] = useState(new Date().getFullYear());
const {
events,
mySchedule,
sideMySchedule,
usersYearlySchedulesLoading,
myPendingScheduleList,
pendingLoading,
setReRender,
setMyPendingScheduleList,
} = useDataFetching(year);
const { isModalOpen, setIsModalOpen } = useLoginModalOpen();
const {
scheduleInput,
contextHolder,
isAddingRequest,
accessToken,
handleSelect,
handleDatePicker,
handleRangePicker,
handleSubmitSchedule,
} = useAddingSchedule(setReRender, setMyPendingScheduleList);
return (
<>
{contextHolder}
{/* 로그인 창 */}
<Modal
title="로그인"
centered
closeIcon={false}
footer={null}
open={isModalOpen}
>
<Signin setIsModalOpen={setIsModalOpen} />
</Modal>
<Layout
style={{
filter: accessToken ? '' : 'blur(5px)',
userSelect: 'none',
height: 'calc(100vh - 60px)',
display: 'flex',
flexDirection: 'row',
}}
>
<Sidebar
handleDatePicker={handleDatePicker}
handleRangePicker={handleRangePicker}
handleSelect={handleSelect}
handleSubmitSchedule={handleSubmitSchedule}
isAddingRequest={isAddingRequest}
myPendingScheduleList={myPendingScheduleList}
pendingLoading={pendingLoading}
scheduleInput={scheduleInput}
setMyPendingScheduleList={setMyPendingScheduleList}
sideMySchedule={sideMySchedule}
usersYearlySchedulesLoading={usersYearlySchedulesLoading}
/>
<Layout style={{ paddingLeft: 10, flex: 1, height: '100%' }}>
<Content
style={{
background: 'white',
height: '100%',
overflow: 'auto',
}}
>
<Calendar
mySchedule={mySchedule}
events={events}
year={year}
setYear={setYear}
usersYearlySchedulesLoading={usersYearlySchedulesLoading}
/>
</Content>
</Layout>
</Layout>
</>
);
}
(hooks)
import { scheduleList } from '@/api/home/scheduleList';
import { AccessTokenAtom } from '@/recoil/AccessTokkenAtom';
import { useEffect, useState } from 'react';
import { SetterOrUpdater, useRecoilState, useRecoilValue } from 'recoil';
import { UserEmailAtom } from '@/recoil/UserEmailAtom';
import dayjs from 'dayjs';
import { DUTY_ANNUAL } from '@/data/constants/commons';
import { pendingList } from '@/api/home/pendingList';
import { ReRenderStateAtom } from '@/recoil/ReRenderStateAtom';
import { DatePickerProps, RangePickerProps } from 'antd/es/date-picker';
import { message } from 'antd';
import { addScheduleRequest } from '@/api/mySchedule';
import { ScheduleItem, mySchedule } from './home.type';
export const useDataFetching = (year: number) => {
const accessToken = useRecoilValue(AccessTokenAtom);
const userEmail = useRecoilValue(UserEmailAtom);
const [events, setEvents] = useState<ScheduleItem[]>([]);
const [reRender, setReRender] = useRecoilState(ReRenderStateAtom);
const [sideMySchedule, setSideMyschedule] = useState<
{
id: number;
key: number;
scheduleType: EmployeeRequestType;
startDate: string;
endDate: string;
state: EmployeeRequestResult;
}[]
>([]);
const [usersYearlySchedulesLoading, setUsersYearlySchedulesLoading] =
useState(false);
useEffect(() => {
const getUsersYearlySchedules = async () => {
if (!accessToken) {
return;
}
try {
setUsersYearlySchedulesLoading(true);
const listResponse = await scheduleList(year);
const listResponseData = listResponse.data.response;
const sideMyScheduleData = listResponseData
.filter((item: mySchedule) => item.userEmail === userEmail)
.map((item: mySchedule) => {
return {
id: item.id,
key: item.id,
scheduleType: item.scheduleType,
startDate: item.startDate,
endDate: item.endDate,
state: item.state,
};
});
setSideMyschedule(sideMyScheduleData);
const events = listResponseData
.filter((item: mySchedule) => item.state === 'APPROVE')
.map((item: ScheduleItem) => {
const adjustEndDate = dayjs(item.endDate)
.add(1, 'day')
.format('YYYY-MM-DD');
return {
userEmail: item.userEmail,
title: item.userName,
start: item.startDate,
end: adjustEndDate,
color: DUTY_ANNUAL[item.scheduleType].color,
};
});
setEvents(events);
} catch (error) {
console.error(error);
} finally {
setUsersYearlySchedulesLoading(false);
}
};
getUsersYearlySchedules();
}, [year, accessToken, userEmail, setUsersYearlySchedulesLoading]);
const mySchedule = events.filter((event) => event.userEmail === userEmail);
const [myPendingScheduleList, setMyPendingScheduleList] = useState<
{
id: number;
key: number;
scheduleType: EmployeeRequestType;
startDate: string;
endDate: string;
state: 'PENDING';
}[]
>([]);
const [pendingLoading, setPendingLoading] = useState(false);
useEffect(() => {
const myPendingSchedule = async () => {
if (!accessToken) {
return;
}
try {
setPendingLoading(true);
const response = await pendingList(year);
const responseData = response.data.response;
const myPendingScheduleData = responseData.map((item: mySchedule) => {
return {
id: item.id,
key: item.id,
scheduleType: item.scheduleType,
startDate: item.startDate,
endDate: item.endDate,
state: item.state,
};
});
setMyPendingScheduleList(myPendingScheduleData);
} catch (error) {
console.error(error);
} finally {
setPendingLoading(false);
}
};
myPendingSchedule();
}, [year, accessToken, reRender]);
return {
events,
usersYearlySchedulesLoading,
sideMySchedule,
mySchedule,
pendingLoading,
myPendingScheduleList,
setReRender,
setMyPendingScheduleList,
};
};
export const useLoginModalOpen = () => {
const accessToken = useRecoilValue(AccessTokenAtom);
const [isModalOpen, setIsModalOpen] = useState<boolean>(!accessToken);
// 로그아웃을 하면 isModalOpen이 !accessToken의 상태를 바로 반영하지 않음
// 따라서 useEffect로 반영이 되도록함
useEffect(() => {
setIsModalOpen(!accessToken);
}, [accessToken]);
return {
isModalOpen,
setIsModalOpen,
};
};
export const useAddingSchedule = (
setReRender: SetterOrUpdater<boolean>,
setMyPendingScheduleList: React.Dispatch<
React.SetStateAction<
{
id: number;
key: number;
scheduleType: EmployeeRequestType;
startDate: string;
endDate: string;
state: 'PENDING';
}[]
>
>,
) => {
const [messageApi, contextHolder] = message.useMessage();
const [isAddingRequest, setIsAddingRequest] = useState(false);
const accessToken = useRecoilValue(AccessTokenAtom);
accessToken;
const [scheduleInput, setScheduleInput] = useState<{
scheduleType: string;
startDate: string;
endDate: string;
}>({
scheduleType: '',
startDate: '',
endDate: '',
});
const handleSelect = (value: string) => {
setScheduleInput({
startDate: '',
endDate: '',
scheduleType: value,
});
};
const handleRangePicker = (value: RangePickerProps['value']) => {
const startDate = value && value[0];
const endDate = value && value[1];
setScheduleInput((prev) => ({
...prev,
startDate: dayjs(startDate).format('YYYY-MM-DD'),
endDate: dayjs(endDate).format('YYYY-MM-DD'),
}));
};
const handleDatePicker = (value: DatePickerProps['value']) => {
const startDate = dayjs(value).format('YYYY-MM-DD');
setScheduleInput((prev) => ({
...prev,
startDate: dayjs(startDate).format('YYYY-MM-DD'),
endDate: dayjs(startDate).format('YYYY-MM-DD'),
}));
};
const handleSubmitSchedule = async () => {
if (!accessToken) {
return;
}
try {
setIsAddingRequest(true);
const response = await addScheduleRequest(scheduleInput);
if (response.status === 200) {
messageApi.open({
type: 'success',
content: `${
DUTY_ANNUAL[
response.data.response.scheduleType as EmployeeRequestType
]?.label
} 신청 완료`,
});
if (response.data.response.scheduleType === 'ANNUAL') {
setReRender((prev) => !prev);
}
if (response.data.response.scheduleType === 'DUTY') {
const newPendingSchedule = {
id: response.data.response.id,
key: response.data.response.id,
scheduleType: response.data.response.scheduleType,
startDate: response.data.response.startDate,
endDate: response.data.response.endDate,
state: response.data.response.state,
};
setMyPendingScheduleList((prev) => {
return [...prev, newPendingSchedule];
});
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
messageApi.open({
type: 'error',
content:
error.response?.data.error.message ||
'연차, 당직 신청 등록 중 오류가 발생하였습니다.',
});
} finally {
setIsAddingRequest(false);
}
};
return {
contextHolder,
isAddingRequest,
accessToken,
handleSelect,
handleRangePicker,
handleDatePicker,
handleSubmitSchedule,
scheduleInput,
};
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
} catch (error: any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
만약 error내부의 속성을 이용해야 하는 경우에는 type지정을 어떻게 하나요?
const handleChangePassword = async (values: {
newPassword: string;
confirmPassword: string;
}) => {
try {
if (!accessToken) {
return;
}
const response = await changeMyInfo({
userPassword: values.newPassword,
});
if (response.status === 200) {
setIsModalOpen(false);
customMessage('비밀번호를 수정하였습니다.');
}
if (formRef.current) {
formRef.current.resetFields();
}
} catch (error) {
customMessage(error.response.data.error.message, 'error'); // 'error' is of type 'unknown'. 에러
}
};
messageApi.open({ | ||
type: 'error', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import { message } from 'antd';
import { NoticeType } from 'antd/es/message/interface';
export const useMessage = () => {
const [messageApi, contextHolder] = message.useMessage();
const customMessage = (content: string, type: NoticeType = 'success') =>
messageApi.open({
type,
content,
});
return { customMessage, contextHolder };
};
customMessage('비밀번호를 수정하였습니다.');
customMessage('비밀번호 수정에 실패하였습니다.', 'error');
if (!accessToken) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interceptor에서 처리했습니다.
KDT5-MINI with backend
프로젝트 소개
본 웹 어플리케이션은 50명 내외의 중소기업에서 사용하는 연차/당직 관리 프로그램입니다.
패스트캠퍼스 백엔드 5기 3분과 팀을 이루어 협업을 진행하였습니다.
결과물 보러가기
리포지토리
11조 개쩌는팀 소개
관리자
캘린더
연차/당직 신청
시작 가이드
Installation
사용한 기술, 라이브러리
Environment
Config
Frontend
Backend
Co-work
화면 구성
로그인
회원가입
메인 화면
내 정보 페이지
내 연차/당직 페이지
관리자 연차/당직 승인 페이지
MANAGER
가 메뉴가 보이지 않으며 접근 할 수 없다.관리자 사원 직책 변경 페이지
고찰
협업
AccessToken & RefreshToken, HttpOnly 쿠키, Axios Interceptor
Authorization
Athentication
AccessToken이 만료 된 경우
HttpOnly 쿠키
withCredentials
를true
로 설정해야 한다.외부 서비스 (cloudinary)
불필요한 리렌더링과 통신
기획 설계 단계에서의 헛점이 여실히 드러나는 부분이었다고 생각드게 불필요한 리렌더링이 일어나는 것과 서버에 요청하는 것이다.
헤더에 사용자 정보에 있는 연차의 상태가 업데이트 될 때 마다 리렌더링 일어나게 하였으나, 연차와 상관없는 당직을 신청하거나 삭제할 때도 같은 상태가 업데이트가 되어서 연차와 당직을 구분하는 로직을 추가하였다.
메인 페이지에서 switch를 토글하면 모든 일정과 사용자의 일정을 필터링해서 볼 수 있게 했는데, 초기에는 토글 할 때 마다 서버에 통신을 해서 모든 일정을 볼 때 1년치 데이터를 가져왔고, 사용자의 일정을 볼 때에도 1년치 데이터를 또 받아온 다음에 사용자 정보 데이터도 받아와서 필터링 하는 구조였었다.
생각해보니 처음에 모든 일정 데이터를 1년 단위로 서버에서 받아왔고, 이 받아온 데이터에는 사용자의 일정도 포함되어있는 상태였다.
서버에서 보내온 1년 치 데이터와 사용자 정보를 받아와서 모든 일정과 사용자 일정을 필터링하는 로직을 추가해서 불필요한 통신을 줄였다.
수정 전
switch토글과 month의 상태가 변할 때 마다 서버에 불필요하게 요청을 함.
scheduleList의 response에 있는 userName과 getMyAccount response에 있는 userName이 같을 시에 필터링 되게 하였었음.
수정 후
scheduleList response로 오는 data스키마를 수정하여 userEmail을 확인 할 수 있게 하였고,
userEmail을 리코일을 통해서 전역으로 관리하여, response의 userEmail같으면 필터링 되게 하였다.
스위치 토글과 month의 상태값 변화를 수정 전과 달리 의존성 배열에서 삭제하였다.
scheduleList는 년 단위의 데이터를 받아오니 월 단위 변경에 대해서 의존성 배열에서 필요없다고 판단하였고,
스위치 토글 또한 처음 받아온 년 단위 데이터를 이용하면 되어서 의존성 배열에서 제거했다.